﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Security.Cryptography;
using Dycox.ComponentModel;
using Dycox.Workflows.Data;

namespace Dycox.Workflows
{
    public abstract partial class Workflow : InitializableObject
    {

        public event ProcessCompletedEventHandler ProcessCompleted;
        public event StageCompletedEventHandler StageCompleted;
        public event CompletingStageEventHandler CompletingStage;
        public event ProcessAbortedEventHandler ProcessAborted;

        public Workflow(string name)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            _name = name;
        }

        private string _name;

        public string Name
        {
            get { return _name; }
        }

        string _displayName;

        public string DisplayName
        {
            get
            {
                if (_displayName == null)
                {
                    DisplayNameAttribute dna = (DisplayNameAttribute)Attribute.GetCustomAttribute(this.GetType(), typeof(DisplayNameAttribute));
                    if (dna != null) return dna.DisplayName;
                    else return _name;
                }
                else
                    return _displayName;
            }
            set
            {
                ThrowIfNotInitializing();
                _displayName = value;
            }
        }

        string _description;

        public string Description
        {
            get
            {
                if (_description == null)
                {
                    DescriptionAttribute da = (DescriptionAttribute)Attribute.GetCustomAttribute(this.GetType(), typeof(DescriptionAttribute));
                    if (da != null) return da.Description;
                    else return null;
                }
                else
                    return _description;
            }
            set
            {
                ThrowIfNotInitializing();
                _description = value;
            }
        }

        Version _currentVersion;

        public Version CurrentVersion
        {
            get
            {
                if (_currentVersion == null)
                    _currentVersion = new Version(1, 0);
                return _currentVersion;
            }
            set
            {
                ThrowIfNotInitializing();
                _currentVersion = value;
            }
        }

        Version _compatibleVersion;

        public Version CompatibleVersion
        {
            get
            {
                if (_compatibleVersion == null)
                {
                    Version current = CurrentVersion;
                    _compatibleVersion = new Version(current.Major, 0);
                }
                return _compatibleVersion;
            }
            set
            {
                ThrowIfNotInitializing();

                if (value != null)
                {
                    Version current = CurrentVersion;
                    if (current == null)
                        throw new InvalidOperationException("Must set CurrentVersion first.");
                    if (value > current)
                        throw new InvalidOperationException("The CompatibleVersion must not higher than CurrentVersion.");
                }

                _compatibleVersion = value;
            }
        }

        private StageCollection _stages;

        public StageCollection Stages
        {
            get
            {
                if (_stages == null)
                    _stages = new StageCollection(this);

                return _stages;
            }
        }

        private TransitionCollection _transtions;

        public TransitionCollection Transitions
        {
            get
            {
                if (_transtions == null)
                    _transtions = new TransitionCollection(this);

                return _transtions;
            }
        }

        private ConditionCollection _completeConditions;

        public ConditionCollection CompleteConditions
        {
            get
            {
                if (_completeConditions == null)
                    _completeConditions = new ConditionCollection();
                return _completeConditions;
            }
        }

        private EntranceStage _entrance;

        public EntranceStage Entrance
        {
            get
            {
                return _entrance;
            }
            internal set
            {
                _entrance = value;
            }
        }

        private PlaceHolderStage _anyStage;

        internal PlaceHolderStage AnyStage
        {
            get
            {
                if (_anyStage == null)
                {
                    _anyStage = new PlaceHolderStage("AnyStage@" + Name, this);
                }
                return _anyStage;
            }
        }

        private NoMoreStepStage _endOfThread;

        internal NoMoreStepStage NoMoreStep
        {
            get
            {
                if (_endOfThread == null)
                    _endOfThread = new NoMoreStepStage("EndOfThread@" + Name, this);
                return _endOfThread;
            }
        }

        public WFProcess CreateProcess(OperationArguments args)
        {
            if (args == null)
                throw new ArgumentNullException("args");

            EntranceExecutionContext wec = new EntranceExecutionContext(this, args);
            var result = Forward(wec);
            return result.Success ? wec.Process : null;
        }

        private void UpdateAppObject(WorkflowExecutionContext context, bool modifying)
        {
            WFProcess proc = context.Process;
            IReflectionRoot target = context.AppObject;

            if (!modifying)
            {
                target.ProcessId = proc.ProcessId;
                target.CreationTime = proc.CreationTime;
                target.StartTime = proc.StartTime;
                target.Status = proc.Status;
                target.WorkflowName = proc.WorkflowName;
                target.FinishTime = proc.FinishTime;
            }

            WFStep[] steps = proc.ActiveSteps;
            Stage[] stages;
            if (steps.Length > 0)
            {
                var coll = Stages;
                stages = steps.Select(o => coll[o.StageName]).Distinct().ToArray();
            }
            else
                stages = new Stage[0];

            target.SetCurrentStages(stages);

            //proc.ActiveStageDisplayNames = target.CurrentStages;
            //proc.AppName = target.AppName;
            //proc.AppKey = target.AppKey;
        }

        //protected virtual void OnProcessCreated(ComponentEventArgs e)
        //{
        //    if (ProcessCreated != null)
        //        ProcessCreated(this, e);
        //}

        protected virtual void OnProcessCompleted(ProcessCompletedEventArgs e)
        {
            if (ProcessCompleted != null)
                ProcessCompleted(this, e);
        }

        protected virtual void OnStageCompleted(StageCompletedEventArgs e)
        {
            if (StageCompleted != null)
                StageCompleted(this, e);
        }

        protected virtual void OnProcessAborted(ProcessAbortedEventArgs e)
        {
            if (ProcessAborted != null)
                ProcessAborted(this, e);
        }

        public bool Inject(OperationArguments args, InjectionStage stage)
        {
            if (args == null)
                throw new ArgumentNullException("args");
            if (stage == null)
                throw new ArgumentNullException("stage");

            if (stage.Workflow != this)
                throw new ArgumentException("invalid_stage_another_workflow");

            return Forward(new InjectionExecutionContext(stage, args)).Success;
        }

        public bool CompleteStage(OperationArguments args)
        {
            if (args == null)
                throw new ArgumentNullException("args");

            return Forward(new WorkflowForwardingContext(this, args)).Success;
        }

        private CompleteStageResult Forward(WorkflowForwardingContext context)
        {
            
            CompleteStageResult result;

            using (context)
            {
                context.BeginExecute();
                result = CompleteStage(context);
                context.EndExecute(result.Success);
            }

            if (result.Success && result.HasUpcomingProcesses)
            {
                if (WorkflowManager.TransactionRequired && !context.DataProvider.TransactionExists)
                    throw new WorkflowException(SR.GetString("transaction_required"));

                foreach (OperationArguments args in result.UpcomingProcessCreationArguments)
                {
                    Workflow wf = WorkflowManager.GetWorkflow(args.WorkflowName, true);
                    args.ComponentID = context.Process.ID;
                    if (args.DataSource != context.Arguments.DataSource)
                        throw new WorkflowException(SR.GetString("invalid_datasource"));
                    
                    wf.CreateProcess(args);
                }

            }

            return result;
        }

        protected virtual void OnCompletingStage(CompletingStageEventArgs e)
        {
            if (CompletingStage != null)
                CompletingStage(this, e);
        }

        //private WFFrame CreateFrame(WorkflowExecutionContext context, FrameType type)
        //{
        //    var stage = context.Stage;
        //    var process = context.Process;
        //    var step = context.Step;

        //    if (stage.Usage == StageUsage.PlaceHolder)
        //        throw new ArgumentException();

        //    if (stage.Usage == StageUsage.Entrance)
        //        step = null;

        //    WFFrame frame = process.NewFrame();
            
        //    frame.CreationTime = WorkflowManager.TimeService.Now;
        //    frame.Type = type;
        //    frame.WorkflowVersion = this.CurrentVersion.ToString();
        //    frame.Description = GetFrameDescription(process, stage, type, step, context.Arguments.AppObject);
        //    frame.WorkflowName = this.Name;
        //    frame.StageName = stage.Name;
        //    frame.StageType = stage.Usage;
        //    frame.StepId = context.Step.ID.ToString();

        //    return frame;
        //}

        //protected virtual string GetFrameDescription(WFProcess process, Stage stage, FrameType frameType, WFStep step, IReflectionRoot appObj)
        //{
        //    string id = "frame_desp_" + frameType.ToString().ToLower();

        //    return SR.GetString(id, this.DisplayName, stage.DisplayName);
        //}

        private CompleteStageResult CompleteStage(WorkflowForwardingContext context)
        {
            CompleteStageResult result = new CompleteStageResult();
            var args = context.Arguments;
            var step = context.Step;
            var contract = args.UserData;
            var refobj = args.AppObject;
            var process = context.Process;
            var stage = context.Stage;

            if (FlowControl.IsComponentFinishedCore(step))
                throw new WorkflowExecutionException(SR.GetString("error_step_finished"));

            WriteStepData(context);

                contract.Apply(args, step, this, false);

            var mfr = Control.MoveForward(context);
            
            CompletingStageEventArgs cse = new CompletingStageEventArgs(mfr);
            OnCompletingStage(cse);
            if (!cse.Cancel)
            {
                if (refobj != null)
                    UpdateAppObject(context, false);

                process.LastStageDisplayName = stage.DisplayName;
                process.LastActiveTime = WorkflowManager.TimeService.Now;

                if (stage is EntranceStage && ((EntranceStage)stage).AutoStartProcess && process.Status != WorkflowComponentStatus.Active && process.Status != WorkflowComponentStatus.Completed)
                    Control.ActivateProcess(step);
                
                StageCompletedEventArgs sce = new StageCompletedEventArgs(mfr);
                OnStageCompleted(sce);
                //if (sce.HasUpcomingProcesses) result.UpcomingProcessCreationArguments.AddRange(sce.UpcomingProcessCreationArguments);

                if (FlowControl.IsComponentFinishedCore(process))
                {
                    var pce = new ProcessCompletedEventArgs();
                    OnProcessCompleted(pce);
                    if (pce.HasUpcomingProcesses) result.UpcomingProcessCreationArguments.AddRange(pce.UpcomingProcessCreationArguments);
                }

                result.Success = true;
            }

            return result;
        }

        private void WriteStepData(WorkflowExecutionContext context)
        {
            IUser user = WorkflowManager.AuthenticationService.CurrentUser;

            Stage stage = context.Stage;
            WFStep step = context.Step;

            if (user != null)
            {
                step.UserName = user.Name;
                step.UserId = user.ID;
            }

            step.SetUserState(context.Arguments.UserData);
            step.OperatorName = context.Arguments.OperatorName ?? step.UserName ?? string.Empty;
            step.Comment = context.Arguments.Comment;

        }

        public void Fallback(OperationArguments args)
        {
            using (DataProvider provider = args.CreateProvider())
            {
                try
                {
                    provider.BeginTransaction();
                    Fallback(args, provider);
                    provider.CommitTransaction();
                }
                catch
                {
                    provider.RollbackTransaction();
                    throw;
                }
            }
        }

        private void Fallback(OperationArguments args, DataProvider provider)
        {
            FallbackContract fc = args.UserData as FallbackContract;
            if (fc == null)
                throw new WorkflowException(SR.GetString("invalid_contract"));

            WFProcess parent = null;
            FrameType frameType;
            bool procRemoved = false;

            using (WorkflowUndoContext wec = new WorkflowUndoContext(this, args, provider))
            {
                wec.BeginExecute();

                var undo = wec.UndoState;

                frameType = wec.Frame.Type;

                provider.DeepDeleteProcess(wec.Process);
                if (undo.Process.FrameCount > 0)
                {
                    provider.Insert(undo.Process);
                }
                else
                {
                    procRemoved = true;
                    if (wec.Process.ParentProcessId != null)
                    {
                        parent = provider.GetProcess(new ComponentIdentifier(wec.Process.ParentProcessId.Value));
                    }
                }
                // 注意：此时的Stage不是引发退回操作的环节，而是其前一个环节。
                wec.Stage.GetContract(wec.Step).Undo(wec);
                
                wec.EndExecute(true);

            }

            if (frameType == FrameType.Bridging)
            {
                if (parent != null)
                {
                    //! TODO: 需要增加一个标识，指示环节是否自动完成。对于自动完成的环节，需要自动退回。
                    var children = provider.GetChildProcesses(parent.ProcessId);
                    if (!children.Any(o => o.ProcessId != args.ComponentID.ProcessId))
                    {
                        Workflow wf = WorkflowManager.GetWorkflow(parent.WorkflowName, true);
                        OperationArguments a = new OperationArguments(args.UserData);
                        a.ComponentID = parent.ID;
                        wf.Fallback(a, provider);
                    }
                }
                else if (!procRemoved)
                {
                    this.Fallback(args, provider);
                }
            }
        }

        public void AbortProcess(OperationArguments args)
        {
            using (var context = new AbortProcessContext(this, args))
            {
                context.BeginExecute();
                AbortProcess(context, true);
                context.EndExecute(context.DataProvider.ShouldDisposeDataSource);
            }
        }

        internal void AbortProcess(AbortProcessContext context, bool updateAppObject)
        {
            if (context.Step == null)
                Control.AbortProcess(context.Process, context.Reason);
            else
                Control.AbortProcess(context, context.Reason);

            if (updateAppObject && context.AppObject != null)
            {
                UpdateAppObject(context, false);
            }

            OnProcessAborted(new ProcessAbortedEventArgs(context));
        }


        //protected virtual void OnStageCompleted(StageDataEventArgs e)
        //{
        //    if (this.StageCompleted != null)
        //        this.StageCompleted(this, e);
        //}

        private FlowControl _control;

        internal FlowControl Control
        {
            get
            {
                if (_control == null)
                    _control = WorkflowManager.CreateControl(this);
                return _control;
            }
        }

        private ThreadLockCollection _threadLocks;

        public ThreadLockCollection ThreadLocks
        {
            get
            {
                if (_threadLocks == null)
                    _threadLocks = new ThreadLockCollection(this);
                return _threadLocks;
            }
        }

        internal protected virtual bool TestCustomCondition(ConditionTestArguments args, Conditions.CustomCondition condition)
        {
            return false;
        }


        protected override void BeginInit()
        {
        }

        protected override void EndInit()
        {
            ValidateWorkflow();
        }

        protected void ValidateWorkflow()
        {
            if (this.Entrance == null)
                throw new WorkflowException("Workflow " + this.Name + " has no entrance.");
        }

        public event PopulateHintEventHandler PopulateHint;

        protected internal virtual void OnPopulateHint(PopulateHintEventArgs e)
        {
            if (PopulateHint != null)
                PopulateHint(this, e);
        }

    }
}
